07.3 精通自定义 View 之 绘图进阶——BlurMaskFilter 发光效果与图片阴影

返回自定义 View 目录

在这张效果图中涉及三个发光效果:文字、图形和位图。

从最后一张美女位图所形成的发光效果中可以看到,与 setShadowLayer() 函数一样,发光效果也只会影响边缘部分图像,内部图像是不受影响的。

从第三幅图像(红绿各一半的位图)中可以看到:发光效果是无法指定发光颜色的,采用边缘部分的颜色取样来进行模糊发光。所以边缘是什么颜色,发出的光也就是什么颜色的。

所以初步我们对发光效果有如下结论:

  • 与 setShadowLayer() 函数一样,发光效果使用的也是高斯模糊算法,并且只会影响边缘部分图像,内部图像是不受影响的。
  • 发光效果是无法指定发光颜色的,采用边缘部分的颜色取样来进行模糊发光。所以边缘是什么颜色,发出的光也就是什么颜色的。

7.3.1 概述

1
public MaskFilter setMaskFilter(MaskFilter maskfilter)

setMaskFilter() 函数中的 MaskFilter 也是没有具体实现的,是通过派生子类来实现具体的不同功能的。MaskFilter 有两个派生类:BlurMaskFilter 和 EmbossMaskFilter。其中,BlurMaskFilter 就能够实现发光效果;而 EmbossMaskFilter 则可以用于实现浮雕效果,用处很少,这里就不再讲解了。另一点需要注意的是,setMaskFilter() 函数是不支持硬件加速的,必须关闭硬件加速才可以。

BlurMaskFilter 的构造函数如下:

1
public BlurMaskFilter(float radius, Blur style)

  • float radius:用来定义模糊半径,同样是高斯模糊算法。
  • Blur style:发光样式,有内 Blur.INNER(内发光)、Blur.SOLID(外发光)、Blur.NORMAL(内外发光)、Blur.OUTER(仅发光部分可见)。

上面效果图的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class TestView extends View {
private Paint mPaint = new Paint();
private Bitmap mHeadBmp;
private Rect mRect;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint.setColor(Color.GREEN);
mPaint.setTextSize(50);
mHeadBmp = BitmapFactory.decodeResource(getResources(), R.drawable.head);
mRect = new Rect();
mPaint.setMaskFilter(new BlurMaskFilter(40, BlurMaskFilter.Blur.SOLID));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText("xian小涛",100,100, mPaint);
canvas.drawCircle(300,300,50, mPaint);
mRect.set(300,500,300 + mHeadBmp.getWidth(),500 + mHeadBmp.getHeight());
canvas.drawBitmap(mHeadBmp,null, mRect, mPaint);
}
}

BlurStyle 发光效果图

依次为:Blur.INNER(内发光)、Blur.SOLID(外发光)、Blur.NORMAL(内外发光)、Blur.OUTER(仅发光部分可见)

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class TestView extends View {
private Paint mPaint;
private BlurMaskFilter inner, solid, normal, outer;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
mPaint.setColor(Color.RED);
inner = new BlurMaskFilter(40, BlurMaskFilter.Blur.INNER);
solid = new BlurMaskFilter(40, BlurMaskFilter.Blur.SOLID);
normal = new BlurMaskFilter(40, BlurMaskFilter.Blur.NORMAL);
outer = new BlurMaskFilter(40, BlurMaskFilter.Blur.OUTER);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setMaskFilter(inner);
canvas.drawCircle(200,200,100, mPaint);
canvas.translate(300, 0);
mPaint.setMaskFilter(solid);
canvas.drawCircle(200,200,100, mPaint);
canvas.translate(300, 0);
mPaint.setMaskFilter(normal);
canvas.drawCircle(200,200,100, mPaint);
canvas.translate(300, 0);
mPaint.setMaskFilter(outer);
canvas.drawCircle(200,200,100, mPaint);
}
}

其中,Blur.OUTER 比较特殊,在这种模式下仅显示发光效果,会把原图像中除发光部分外的其他部分全部变为透明。

7.3.2 给图片添加纯色阴影

大家是否可以看出来发光效果与 setShadowLayer() 函数所生成的阴影之间有什么联系?

先来分析一下 setShadowLayer() 函数的阴影形成过程(假定阴影画笔是灰色)。对于文字和图形,它首先产生一个跟原型一样的灰色副本。然后对这个灰色副本应用 BlurMaskFilter,使其内外发光;这样就形成了所谓的阴影,当然最后再偏移一段距离。

所以,我们要给图片添加灰色阴影效果,就可以仿照这个过程:先绘制一幅跟图片一样大小的灰色图像,然后给这个灰色图形应用 BlurMaskFilter 使其内外发光,最后偏移原图形一段距离绘制阴影。

这里涉及到三个点:

  • 绘制一幅跟图片一样大小的灰色图像。
  • 对灰色图像应用 BlurMaskFilter 使其内外发光。
  • 偏移原图形一段距离绘制阴影。

1. 抽取灰色图像

首先来看怎么能绘出一个指定位图所对应的灰色图像。我们知道 canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) 中的画笔颜色对画出来的位图是没有任何影响的,所以,如果我们需要画一张对应的灰色图像,就需要新建一张一样大小的空白图,而且新图片的透明度要与原图片保持一致。这样一来,如何从原图片中抽出 Alpha 值成为关键。即我们只需要创建一个与原图片一样大小且 Alpha 相同的图片即可。

其实,Bitmap 中已经存在抽取出只具有 Alpha 值图片的函数:

1
public Bitmap extractAlpha();

这个函数的功能是:新建一张空白图片,该图片具有与原图片一样的 Alpha 值,把这个新建的 Bitmap 作为结果返回。这个空白图片中每个像素都具有与原图片一样的 Alpha 值,而且具体的颜色是在使用canvas.drawBitmap() 函数绘制时由传入的画笔颜色指定的。

总结:extractAlpha() 会新建一幅仅具有 Alpha 值的空白图像,而且这张图像的颜色是在使用 canvas.drawBitmap() 函数绘制时传入的画笔颜色指定的。

下面拿一张图片来做实验,这张 PNG 图片中,只有一只小狗,其余地方都是透明色。

下面分别利用 extractAlpha() 函数画出该图片所对应的灰色和黑色阴影,效果图如下所示。

原图、灰色阴影、黑色阴影

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class TestView extends View {
private Paint mPaint;
private Bitmap mBitmap, mAlphaBmp;
private Rect mRect;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
mRect = new Rect();
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
mAlphaBmp = mBitmap.extractAlpha();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = 200;
int height = width * mAlphaBmp.getHeight() / mAlphaBmp.getWidth();
// 绘制原图
mRect.set(50, 50, 50 + width, 50 + height);
canvas.drawBitmap(mBitmap, null, mRect, mPaint);
// 绘制灰色阴影
canvas.translate(width + 100, 0);
mPaint.setColor(Color.GRAY);
canvas.drawBitmap(mAlphaBmp, null, mRect, mPaint);
// 绘制黑色阴影
canvas.translate(width + 100, 0);
mPaint.setColor(Color.BLACK);
canvas.drawBitmap(mAlphaBmp, null, mRect, mPaint);
}
}

2. 绘制阴影

在上面灰色纯色图像的基础上,将此灰色图像使用 BlurMaskFilter 使其内外发光。然后再在灰色模糊阴影的基础上画上原图像,就形成了模糊阴影。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class TestView extends View {
private Paint mPaint;
private Bitmap mBitmap, mAlphaBmp;
private Rect mRect;
private BlurMaskFilter mFilter;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
mRect = new Rect();
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
mAlphaBmp = mBitmap.extractAlpha();
mFilter = new BlurMaskFilter(30, BlurMaskFilter.Blur.NORMAL);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = 200;
int height = width * mAlphaBmp.getHeight() / mAlphaBmp.getWidth();
mRect.set(50, 50, 50 + width, 50 + height);
// 绘制灰色阴影
mPaint.setColor(Color.GRAY);
mPaint.setMaskFilter(mFilter);
canvas.drawBitmap(mAlphaBmp, null, mRect, mPaint);
// 绘制原图
mPaint.setMaskFilter(null);
canvas.translate(-10, -10);
canvas.drawBitmap(mBitmap, null, mRect, mPaint);
}
}

效果如下图所示。

7.3.3 封装控件

将它封装成一个控件,具有如下功能:

  • 让用户定义图片内容。
  • 让用户定义偏移距离。
  • 让用户定义阴影颜色和阴影模糊程度。
  • 可以使用 wrap_content 属性自适应大小。

下面程序经过多次测试,效果并不完美,待完善。

res/values/xshadow.xml

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="XShadowImageView">
<attr name="src" format="reference"/>
<attr name="shadowDx" format="integer" />
<attr name="shadowDy" format="integer" />
<attr name="shadowColor" format="color"/>
<attr name="shadowRadius" format="float"/>
</declare-styleable>
</resources>

src/…/XShadowImageView.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class XShadowImageView extends View {
private Paint mPaint;
private Bitmap mBitmap, mShadowBitmap;
private int mDx, mDy;
private float mRadius;
private int mShadowColor;
private BlurMaskFilter mBlurMaskFilter;
private Rect mRect;
public XShadowImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public XShadowImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, @Nullable AttributeSet attrs) {
// 禁用硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, null);
// 提取属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.XShadowImageView);
mDx = ta.getInt(R.styleable.XShadowImageView_shadowDx, 0);
mDy = ta.getInt(R.styleable.XShadowImageView_shadowDy, 0);
mRadius = ta.getFloat(R.styleable.XShadowImageView_shadowRadius, 0);
mShadowColor = ta.getInt(R.styleable.XShadowImageView_shadowColor, Color.BLACK);
int bitmapId = ta.getResourceId(R.styleable.XShadowImageView_src, -1);
if (bitmapId != -1) {
mBitmap = BitmapFactory.decodeResource(getResources(), bitmapId);
}
ta.recycle();
// 其他初始化
mRect = new Rect();
mPaint = new Paint();
mPaint.setColor(mShadowColor);
mBlurMaskFilter = new BlurMaskFilter(mRadius, BlurMaskFilter.Blur.NORMAL);
if (mBitmap != null) {
mShadowBitmap = mBitmap.extractAlpha();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int width = mBitmap.getWidth();
int height = mBitmap.getHeight();
width = (measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width;
height = (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height;
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mBitmap != null) {
int width = getWidth() - mDx;
int height = getHeight() - mDy;
// 绘制阴影
mPaint.setMaskFilter(mBlurMaskFilter);
mRect.set(mDx, mDy, width, height);
canvas.drawBitmap(mShadowBitmap, null, mRect, mPaint);
// 绘制原图像
mPaint.setMaskFilter(null);
mRect.set(0, 0, width, height);
canvas.drawBitmap(mBitmap, null, mRect, mPaint);
}
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<com.xxt.xtest.XShadowImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center_horizontal"
app:src="@drawable/head"
app:shadowDx="30"
app:shadowDy="30"
app:shadowRadius="30.0"
app:shadowColor="@android:color/black"/>
<com.xxt.xtest.XShadowImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
app:src="@drawable/head"
app:shadowDx="30"
app:shadowDy="30"
app:shadowRadius="30.0"
app:shadowColor="@android:color/holo_red_dark"/>
<com.xxt.xtest.XShadowImageView
android:layout_width="260dp"
android:layout_height="360dp"
android:layout_gravity="center_horizontal"
app:src="@drawable/meinv"
app:shadowDx="40"
app:shadowDy="40"
app:shadowRadius="40.0"
app:shadowColor="@android:color/holo_red_dark"/>
</LinearLayout>